unit database;

{< The code is mostly mine, but the parameter and effect names are taken form
tkoolmv corescript by Kadokawa, MIT-licensed. According to my best knowledge,
constant names are not copyrightable, but this needs check. }

{$mode objfpc}{$H+}

interface

uses
  Classes, SysUtils, FileUtil, fpjson, jsonparser, fgl;

const
  //Trait names are adapted from Game_BattlerBase.js in corescript
  { Trait code for trait, specifying an element rate (effectiveness of elements
    when attacking an actor or enemy). value would be from 0.0 to 9999.99 (from
    0% to 999999%), dataId is the element ID. }
  TRAIT_ELEMENT_RATE   = 11;
  { Trait code for the trait specifying the probability of lowering a param.
   value is from 0.0 to 9999.99, dataId is the parameter ID. }
  TRAIT_DEBUFF_RATE    = 12;
  { Trait code for the trait specifying the probability of giving a certain
   state. value is from 0.0 to 9999.99, dataId is the state ID. }
  TRAIT_STATE_RATE     = 13;
  { Trait code for the trait disabling certain state for the actor. dataId is
   the state ID. value is unused. }
  TRAIT_STATE_RESIST   = 14;

  { Trait code for the Param multiplier. Param values set in Class are
    multiplied with 'value' members of traits. dataId is the param ID.
    For each trait, value is from 0.0 to 9999.99 }
  TRAIT_PARAM          = 21;
  { Trait code for the ExParam augend. Param values are summed to get
    the final value. dataId is the ex param ID. For each trait, value is
    from -9999.99 to 99999.99}
  TRAIT_XPARAM         = 22;
  { Trait code for the SpParam multiplier. The initial value is 1.0 (100%).
    Param values are multiplied with 'value' members of traits. dataId is
    the sp param ID. value is from 0.0 to 9999.99 }
  TRAIT_SPARAM         = 23;

  TRAIT_ATTACK_ELEMENT = 31;
  TRAIT_ATTACK_STATE   = 32;
  TRAIT_ATTACK_SPEED   = 33;
  TRAIT_ATTACK_TIMES   = 34;

  { Trait code to allow using certain skill type. dataId is the skill type. }
  TRAIT_STYPE_ADD      = 41;
  { Trait code to make certain skill type (temporarily) unavailable. dataId
    is the skill type. }
  TRAIT_STYPE_SEAL     = 42;
  { Trait to add a certain skill (while the trait is applicable). dataId is the
    skill ID }
  TRAIT_SKILL_ADD      = 43;
  { Trait to (temporarily) forbid using a certain skill. dataId is the
    skill ID }
  TRAIT_SKILL_SEAL     = 44;

  { Trait code for the trait that allows equipping a certain weapon type.
   dataId is the weapon type. }
  TRAIT_EQUIP_WTYPE    = 51;
  { Trait code for the trait that allows equipping a certain armour type.
   dataId is the armour type. }
  TRAIT_EQUIP_ATYPE    = 52;
  { Trait code for the trait that prevents players from changing the character's
   equipment. dataId is the equipment type. }
  TRAIT_EQUIP_LOCK     = 53;
  { Prevents players from equipping certain equipment types. dataId is the
   equipment type. }
  TRAIT_EQUIP_SEAL     = 54;
  { Allows to change the second equipment type ('shield') into the second
   weapon type ('armor'). }
  TRAIT_SLOT_TYPE      = 55;

  TRAIT_ACTION_PLUS    = 61;
  TRAIT_SPECIAL_FLAG   = 62;
  TRAIT_COLLAPSE_TYPE  = 63;
  TRAIT_PARTY_ABILITY  = 64;



  EFFECT_RECOVER_HP       = 11;
  EFFECT_RECOVER_MP       = 12;
  EFFECT_GAIN_TP          = 13;
  EFFECT_ADD_STATE        = 21;
  EFFECT_REMOVE_STATE     = 22;
  EFFECT_ADD_BUFF         = 31;
  EFFECT_ADD_DEBUFF       = 32;
  EFFECT_REMOVE_BUFF      = 33;
  EFFECT_REMOVE_DEBUFF    = 34;
  EFFECT_SPECIAL          = 41;
  EFFECT_GROW             = 42;
  EFFECT_LEARN_SKILL      = 43;
  EFFECT_COMMON_EVENT     = 44;

  SPECIAL_EFFECT_ESCAPE   = 0;

  HITTYPE_CERTAIN         = 0;
  HITTYPE_PHYSICAL        = 1;
  HITTYPE_MAGICAL         = 2;

resourcestring
  NormalAttackState = 'Normal attack state';
  EPHitRate = 'Hit rate';
  EPEvasionRate = 'Evasion rate';
  EPCriticalHitRate = 'Critical hit rate';
  EPCriticalEvasionRate = 'Critical evasion rate';
  EPMagicEvasionRate = 'Magic evasion rate';
  EPMagicReflectionRate = 'Magic reflection rate';
  EPCounterAttackRate = 'Counter-attack rate';
  EPHpRegenerationRate = 'HP regeneration rate';
  EPMpRegenerationRate = 'MP regeneration rate';
  EPTpRegenerationRate = 'TP regeneration rate';

  SPTargetRate = 'Target rate';
  SPGuardRate = 'Guard effect rate';
  SPRecoveryEffectRate = 'Recovery effect rate';
  SPPharma = 'Pharmacology';
  SPMpCostRate = 'MP cost rate';
  SPTpCostRate = 'TP charge rate';
  SPPhysicalDamageRate = 'Physical damage rate';
  SPMagicalDamageRate = 'Magical damage rate';
  SPFloorDamageRate = 'Floor damage rate';
  SPExpRate = 'Experience rate';

  FlagAutoBattle = 'Auto battle';
  FlagGuard = 'Guard';
  FlagSubstitute = 'Substitute';
  FlagPreserveTp = 'Preserve TP';

  KnockoutStandard = 'Standard disappearance';
  KnockoutBoss = 'Boss disappearance';
  KnockoutImmediate = 'Immediately disappear';
  KnockoutDoesnt = 'Doesn''t disappear';

  PFlagHalfEncounters = 'Encounter rate = 50%';
  PFlagNoEncounters = 'Encounter rate = 0';
  PFlagNoEnemySurprises = 'No enemy surprise attacks';
  PFlagNoPartyPreemptive = 'No party preemptive attacks';
  PFlagDoubleGold = 'Double gold';
  PFlagDoubleItems = 'Double items';

const

  //Ex-Param names are adapted from Game_BattlerBase.js in corescript
  ExParamNames: array [0..9] of String = (
    EPHitRate,
    EPEvasionRate,
    EPCriticalHitRate,
    EPCriticalEvasionRate,
    EPMagicEvasionRate,
    EPMagicReflectionRate,
    EPCounterAttackRate,
    EPHpRegenerationRate,
    EPMpRegenerationRate,
    EPTpRegenerationRate
  );

  SpParamNames: array [0..9] of String = (
    SPTargetRate,
    SPGuardRate,
    SPRecoveryEffectRate,
    SPPharma,
    SPMpCostRate,
    SPTpCostRate,
    SPPhysicalDamageRate,
    SPMagicalDamageRate,
    SPFloorDamageRate,
    SPExpRate
  );

  SpecialFlagNames: array [0..3] of String = (
    FlagAutoBattle,
    FlagGuard,
    FlagSubstitute,
    FlagPreserveTp
  );

  KnockoutEffectFlagNames: array [0..3] of String = (
    KnockoutStandard,
    KnockoutBoss,
    KnockoutImmediate,
    KnockoutDoesnt
  );

  PartyFlagFlagNames: array [0..5] of String = (
    PFlagHalfEncounters,
    PFlagNoEncounters,
    PFlagNoEnemySurprises,
    PFlagNoPartyPreemptive,
    PFlagDoubleGold,
    PFlagDoubleItems
  );

type
  TIntList = specialize TFPGList<Integer>;

  CreateElementFn = function (ID: Integer): TJSONObject of object;

  { TDatabase }

  TDatabase = class
    private
      Path: String;
      function LoadDbFile(Filename: String): TJSONData;
      procedure LoadDbArray(var Arr: TJSONArray; Filename: String);
      procedure SaveDbArray(Arr: TJSONArray; Filename: String);
      procedure SaveDbData(Obj: TJSONData; Filename: String);
      function GetNameFromSystemArray(ArrayName: String; Id: Integer): String;
      function GetNameFromDbArray(Arr: TJSONArray; Id: Integer; UseLongName: Boolean = True): String;
    public
      Actors: TJSONArray;
      Classes: TJSONArray;
      Skills: TJSONArray;
      Items: TJSONArray;
      Weapons: TJSONArray;
      Armours: TJSONArray;
      Enemies: TJSONArray;
      Troops: TJSONArray;
      States: TJSONArray;
      Animations: TJSONArray;
      Tilesets: TJSONArray;
      CommonEvents: TJSONArray;

      System: TJSONObject;

      destructor Destroy; override;
      procedure Open(APath: String);
      procedure Save;
      procedure SetData(ADb: TDatabase);
      procedure ResizeDbArray(DbArr: TJSONArray; ACreateElementFn: CreateElementFn; NewMaximum: Integer);
      procedure ResizeActors(NewMaximum: Integer);
      function CreateEmptyActor(ID: Integer): TJSONObject;
      procedure ResizeClasses(NewMaximum: Integer);
      function CreateEmptyClass(ID: Integer): TJSONObject;
      procedure ResizeSkills(NewMaximum: Integer);
      function CreateEmptySkill(ID: Integer): TJSONObject;
      procedure ResizeItems(NewMaximum: Integer);
      function CreateEmptyItem(ID: Integer): TJSONObject;
      procedure ResizeWeapons(NewMaximum: Integer);
      function CreateEmptyWeapon(ID: Integer): TJSONObject;
      procedure ResizeArmours(NewMaximum: Integer);
      function CreateEmptyArmour(ID: Integer): TJSONObject;
      procedure ResizeEnemies(NewMaximum: Integer);
      function CreateEmptyEnemy(ID: Integer): TJSONObject;
      procedure ResizeTroops(NewMaximum: Integer);
      function CreateEmptyTroop(ID: Integer): TJSONObject;
      procedure ResizeStates(NewMaximum: Integer);
      function CreateEmptyTileset(ID: Integer): TJSONObject;
      procedure ResizeTilesets(NewMaximum: Integer);
      function CreateEmptyCommonEvent(ID: Integer): TJSONObject;
      procedure ResizeCommonEvents(NewMaximum: Integer);
      function CreateEmptyState(ID: Integer): TJSONObject;
      function GetNames(Arr: TJSONArray): TStringList;

      function GetActorName(Id: Integer; UseLongName: Boolean = True): String;
      function GetItemName(Id: Integer; UseLongName: Boolean = True): String;
      function GetArmourName(Id: Integer; UseLongName: Boolean = True): String;
      function GetWeaponName(Id: Integer; UseLongName: Boolean = True): String;
      function GetEnemyName(Id: Integer; UseLongName: Boolean = True): String;
      function GetTroopName(Id: Integer; UseLongName: Boolean = True): String;
      function GetClassName(Id: Integer; UseLongName: Boolean = True): String;
      function GetTilesetName(Id: Integer; UseLongName: Boolean = True): String;
      function GetElementName(Id: Integer): String;
      function GetParamName(Id: Integer): String;
      function GetStateName(Id: Integer): String;
      function GetArmourTypeName(Id: Integer): String;
      function GetWeaponTypeName(Id: Integer): String;
      function GetVariableName(Id: Integer): String;
      function GetSwitchName(Id: Integer): String;
      function GetEquipTypeName(Id: Integer): String;
      function GetSkillTypeName(Id: Integer): String;
      function GetSkillName(Id: Integer; UseLongName: Boolean = True): String;
      function GetCommonEventName(Id: Integer; UseLongName: Boolean = True): String;
      function GetAnimName(Id: Integer; UseLongName: Boolean = True): String;
      function GetExParamName(Id: Integer): String;
      function GetSpParamName(Id: Integer): String;
      function GetSpecialFlagName(Id: Integer): String;
      function GetKnockoutEffectName(Id: Integer): String;
      function GetPartyFlagName(Id: Integer): String;
      function IsSvBattle: Boolean;


      { Get data IDs for all the traits with the given trait code related to a
        certain actor. This includes traits from the actor, their
        class. Equipment is not taken into account. }
      function GetActorDataIdsForTrait(ActorId: Integer; TraitCode: Integer): TIntList;

      { Returns list of weapon type IDs that the actor can equip. Only actor and
        class traits are taken into account.}
      function GetActorWeaponTypes(ActorId: Integer): TIntList;

      { Returns list of armour type IDs that the actor can equip. Only actor and
        class traits are taken into account. }
      function GetActorArmourTypes(ActorId: Integer): TIntList;

      { Check if the given actor has Two-weapon style. }
      function ActorHasTwoWeapons(ActorId: Integer): Boolean;

      { Long name always includes ID and is intended for use in item lists. Short
        name doesn't include an ID. }
      class function FormatObjName(Obj: TJSONObject; UseLongName: Boolean = True): String;
  end;

  DbResizerProcedure = procedure(NewMaximum: Integer) of object;

implementation

uses
  echelper, autotilehelper, constants;

procedure TDatabase.Open(APath: String);
begin
  Path := APath;

  LoadDbArray(Actors, 'Actors');
  LoadDbArray(Classes, 'Classes');
  LoadDbArray(Skills, 'Skills');
  LoadDbArray(Items, 'Items');
  LoadDbArray(Weapons, 'Weapons');
  LoadDbArray(Armours, 'Armors');
  LoadDbArray(Enemies, 'Enemies');
  LoadDbArray(Troops, 'Troops');
  LoadDbArray(States, 'States');
  LoadDbArray(Animations, 'Animations');
  LoadDbArray(Tilesets, 'Tilesets');
  LoadDbArray(CommonEvents, 'CommonEvents');

  if System <> nil then
     System.Free;
  System := (LoadDbFile('System') as TJSONObject);
end;

procedure TDatabase.Save;
begin
  SaveDbArray(Actors, 'Actors');
  SaveDbArray(Classes, 'Classes');
  SaveDbArray(Skills, 'Skills');
  SaveDbArray(Items, 'Items');
  SaveDbArray(Weapons, 'Weapons');
  SaveDbArray(Armours, 'Armors');
  SaveDbArray(Enemies, 'Enemies');
  SaveDbArray(Troops, 'Troops');
  SaveDbArray(States, 'States');
  SaveDbArray(Animations, 'Animations');
  SaveDbArray(Tilesets, 'Tilesets');
  SaveDbArray(CommonEvents, 'CommonEvents');
  SaveDbData(System, 'System');
end;



function TDatabase.LoadDbFile(Filename: String): TJSONData;
var
  Stream: TFileStream;
  Parser: TJSONParser;
  FullFilename: String;
begin
  FullFilename := Path + DirectorySeparator + Filename + '.json'; //AppendPathDelim(Path) + Filename;
  try
    Stream := TFileStream.Create(FullFilename, fmOpenRead);
    Parser := TJSONParser.Create(Stream);
    try
      LoadDbFile := Parser.Parse;
    finally
      Parser.Free;
    end;
  finally
    Stream.Free;
  end;
end;

procedure TDatabase.SaveDbArray(Arr: TJSONArray; Filename: String);
var
  Stream: TFileStream;
  StringData: UTF8String;
  FullFilename: String;
  I: Integer;
begin
  FullFilename := Path + DirectorySeparator + Filename + '.json'; //AppendPathDelim(Path) + Filename;
  Stream := TFileStream.Create(FullFilename, fmCreate or fmShareExclusive);
  try
    StringData := '[' + LineEnding + 'null';
    Stream.Write(StringData[1], Length(StringData));

    if Arr.Count > 1 then begin
      StringData := ',' + LineEnding;
      Stream.Write(StringData[1], Length(StringData));
    end;

    for I := 1 to Arr.Count -1 do begin
      StringData := Arr[I].AsJSON;
      Stream.Write(StringData[1], Length(StringData));

      if I < Arr.Count -1 then begin
        StringData := ',' + LineEnding;
        Stream.Write(StringData[1], Length(StringData));
      end;
    end;

    StringData := ']';
    Stream.Write(StringData[1], Length(StringData));
  finally
    Stream.Free;
  end;
end;

procedure TDatabase.SaveDbData(Obj: TJSONData; Filename: String);
var
  Stream: TFileStream;
  StringData: UTF8String;
  FullFilename: String;
begin
  FullFilename := Path + DirectorySeparator + Filename + '.json'; //AppendPathDelim(Path) + Filename;
  Stream := TFileStream.Create(FullFilename, fmCreate or fmShareExclusive);
  try
    StringData := Obj.AsJSON;
    Stream.Write(StringData[1], Length(StringData));
  finally
    Stream.Free;
  end;
end;

procedure TDatabase.LoadDbArray(var Arr: TJSONArray; Filename: String);
begin
  if Arr <> nil then begin
    Arr.Free;
  end;

  Arr := LoadDbFile(Filename) as TJSONArray;
end;

procedure TDatabase.SetData(ADb: TDatabase);
begin
  if ADb.Actors <> nil then
     Actors := ADb.Actors.Clone as TJSONArray;
  if ADb.Classes <> nil then
     Classes := ADb.Classes.Clone as TJSONArray;
  if ADb.Skills <> nil then
     Skills := ADb.Skills.Clone as TJSONArray;
  if ADb.Items <> nil then
     Items := ADb.Items.Clone as TJSONArray;
  if ADb.Weapons <> nil then
     Weapons := ADb.Weapons.Clone as TJSONArray;
  if ADb.Armours <> nil then
     Armours := ADb.Armours.Clone as TJSONArray;
  if ADb.Enemies <> nil then
     Enemies := ADb.Enemies.Clone as TJSONArray;
  if ADb.Troops <> nil then
     Troops := ADb.Troops.Clone as TJSONArray;
  if ADb.States <> nil then
     States := ADb.States.Clone as TJSONArray;
  if ADb.Animations <> nil then
     Animations := ADb.Animations.Clone as TJSONArray;
  if ADb.Tilesets <> nil then
     Tilesets := ADb.Tilesets.Clone as TJSONArray;
  if ADb.CommonEvents <> nil then
     CommonEvents := ADb.CommonEvents.Clone as TJSONArray;

  if ADb.System <> nil then
     System := ADb.System.Clone as TJSONObject;
end;

procedure TDatabase.ResizeDbArray(DbArr: TJSONArray;
  ACreateElementFn: CreateElementFn; NewMaximum: Integer);
begin
  while NewMaximum < DbArr.Count -1 do begin
    DbArr.Remove(DbArr[DbArr.Count -1]);
  end;

  while NewMaximum > DbArr.Count -1 do begin
    DbArr.Add(ACreateElementFn(DbArr.Count));
  end
end;

procedure TDatabase.ResizeActors(NewMaximum: Integer);
begin
  ResizeDbArray(Actors, @CreateEmptyActor, NewMaximum);
end;

function TDatabase.CreateEmptyActor(ID: Integer): TJSONObject;
var
  Actor: TJSONObject;
  Equips: TJSONArray;
  I: Integer;
begin
  Actor := TJSONObject.Create;
  Actor.Add('id', ID);
  Actor.Add('battlerName', '');
  Actor.Add('characterIndex', '0');
  Actor.Add('characterName', '');
  Actor.Add('classId', 1);

  Equips := TJSONArray.Create;
  for I := 1 to System.Arrays['equipTypes'].Count -1 do begin
    Equips.Add(0);
  end;
  Actor.Add('equips', Equips);

  Actor.Add('faceIndex', 0);
  Actor.Add('faceName', '');

  Actor.Add('traits', TJSONArray.Create);
  Actor.Add('initialLevel', 1);
  Actor.Add('maxLevel', 99);
  Actor.Add('name', '');
  Actor.Add('nickname', '');
  Actor.Add('note', '');
  Actor.Add('profile', '');

  CreateEmptyActor := Actor;
end;

procedure TDatabase.ResizeClasses(NewMaximum: Integer);
begin
  ResizeDbArray(Classes, @CreateEmptyClass, NewMaximum);
end;

function TDatabase.CreateEmptyClass(ID: Integer): TJSONObject;
var
  NewClass: TJSONObject;
  ExpParams, Traits, Learnings, Params, ParamValues: TJSONArray;
  I, J: Integer;
begin
  NewClass := TJSONObject.Create;
  NewClass.Add('id', ID);

  ExpParams := TJSONArray.Create;
  //TODO: choose different defaults???
  ExpParams.Add(30);
  ExpParams.Add(20);
  ExpParams.Add(30);
  ExpParams.Add(30);
  NewClass.Add('expParams', ExpParams);

  Traits := TJSONArray.Create;
  //TODO: add some default traits???
  NewClass.Add('traits', Traits);

  Learnings := TJSONArray.Create;
  NewClass.Add('learnings', Learnings);
  NewClass.Add('name', '');
  NewClass.Add('note', '');

  Params := TJSONArray.Create;
  for I := 1 to 8 do begin
    ParamValues := TJSONArray.Create;
    for J := 1 to 100 do
      ParamValues.Add(J);
    Params.Add(ParamValues);
  end;
  NewClass.Add('params', Params);

  CreateEmptyClass := NewClass;
end;

procedure TDatabase.ResizeSkills(NewMaximum: Integer);
begin
  ResizeDbArray(Skills, @CreateEmptySkill, NewMaximum);
end;

function TDatabase.CreateEmptySkill(ID: Integer): TJSONObject;
var
  NewSkill, Damage: TJSONObject;
  Effects: TJSONArray;
begin
  NewSkill := TJSONObject.Create;
  NewSkill.Add('id', ID);
  NewSkill.Add('animationId', 0);

  Damage := TJSONObject.Create;
  Damage.Add('critical', False);
  Damage.Add('elementId', 0);
  Damage.Add('formula', '0');
  Damage.Add('type', 0);
  Damage.Add('variance', 20);
  NewSkill.Add('damage', Damage);

  NewSkill.Add('description', '');

  Effects := TJSONArray.Create;
  NewSkill.Add('effects', Effects);
  //TODO: do I need to free it?

  NewSkill.Add('hitType', 0);
  NewSkill.Add('iconIndex', 0);
  NewSkill.Add('message1', '');
  NewSkill.Add('message2', '');
  NewSkill.Add('mpCost', 0);
  NewSkill.Add('name', '');
  NewSkill.Add('note', '');
  NewSkill.Add('occasion', 0);
  NewSkill.Add('repeats', 1);
  NewSkill.Add('requiredWtypeId1', 0);
  NewSkill.Add('requiredWtypeId2', 0);
  NewSkill.Add('scope', 1);
  NewSkill.Add('speed', 0);
  NewSkill.Add('stypeId', 1);
  NewSkill.Add('successRate', 100);
  NewSkill.Add('tpCost', 0);
  NewSkill.Add('tpGain', 0);

  CreateEmptySkill := NewSkill;
end;

procedure TDatabase.ResizeItems(NewMaximum: Integer);
begin
    ResizeDbArray(Items, @CreateEmptyItem, NewMaximum);
end;

function TDatabase.CreateEmptyItem(ID: Integer): TJSONObject;
var
  NewItem, Damage: TJSONObject;
  Effects: TJSONArray;
begin
  NewItem := TJSONObject.Create;
  NewItem.Add('id', ID);
  NewItem.Add('animationId', 0);
  NewItem.Add('consumable', True);

  Damage := TJSONObject.Create;
  Damage.Add('critical', False);
  Damage.Add('elementId', 0);
  Damage.Add('formula', '0');
  Damage.Add('type', 0);
  Damage.Add('variance', 20);
  NewItem.Add('damage', Damage);

  NewItem.Add('description', '');

  Effects := TJSONArray.Create;
  NewItem.Add('effects', Effects);

  NewItem.Add('hitType', 0);
  NewItem.Add('iconIndex', 0);
  NewItem.Add('itypeId', 1);

  NewItem.Add('name', '');
  NewItem.Add('note', '');
  NewItem.Add('occasion', 0);
  NewItem.Add('price', 0);
  NewItem.Add('repeats', 1);
  NewItem.Add('scope', 1);
  NewItem.Add('speed', 0);
  NewItem.Add('successRate', 100);
  NewItem.Add('tpGain', 0);

  CreateEmptyItem := NewItem;
end;

procedure TDatabase.ResizeWeapons(NewMaximum: Integer);
begin
  ResizeDbArray(Weapons, @CreateEmptyWeapon, NewMaximum);
end;

function TDatabase.CreateEmptyWeapon(ID: Integer): TJSONObject;
var
  NewWeapon: TJSONObject;
  Traits: TJSONArray;
begin
  NewWeapon := TJSONObject.Create;
  NewWeapon.Add('id', ID);
  NewWeapon.Add('animationId', 0);
  NewWeapon.Add('description', '');
  NewWeapon.Add('etypeId', 1);

  Traits := TJSONArray.Create;
  Traits.Add(TJSONObject.Create(['code', TRAIT_ATTACK_ELEMENT, 'dataId', 1, 'value', 0]));
  Traits.Add(TJSONObject.Create(['code', TRAIT_XPARAM, 'dataId', 0, 'value', 0]));
  NewWeapon.Add('traits', Traits);

  NewWeapon.Add('iconIndex', 0);
  NewWeapon.Add('name', '');
  NewWeapon.Add('note', '');
  NewWeapon.Add('params', TJSONArray.Create([0, 0, 0, 0, 0, 0, 0, 0]));
  NewWeapon.Add('price', 0);
  NewWeapon.Add('wtypeId', 0);

  CreateEmptyWeapon := NewWeapon;
end;

procedure TDatabase.ResizeArmours(NewMaximum: Integer);
begin
  ResizeDbArray(Armours, @CreateEmptyArmour, NewMaximum);
end;

function TDatabase.CreateEmptyArmour(ID: Integer): TJSONObject;
var
  NewArmour: TJSONObject;
  Traits: TJSONArray;
begin
  NewArmour := TJSONObject.Create;
  NewArmour.Add('id', ID);
  NewArmour.Add('atypeId', 0);
  NewArmour.Add('description', '');
  NewArmour.Add('etypeId', 2);

  Traits := TJSONArray.Create;
  Traits.Add(TJSONObject.Create(['code', TRAIT_XPARAM, 'dataId', 1, 'value', 0]));
  NewArmour.Add('traits', Traits);

  NewArmour.Add('iconIndex', 0);
  NewArmour.Add('name', '');
  NewArmour.Add('note', '');
  NewArmour.Add('params', TJSONArray.Create([0, 0, 0, 0, 0, 0, 0, 0]));
  NewArmour.Add('price', 0);

  CreateEmptyArmour := NewArmour;
end;

procedure TDatabase.ResizeEnemies(NewMaximum: Integer);
begin
  ResizeDbArray(Enemies, @CreateEmptyEnemy, NewMaximum);
end;

function TDatabase.CreateEmptyEnemy(ID: Integer): TJSONObject;
var
  NewEnemy: TJSONObject;

  function CreateActions: TJSONArray;
  var
    Action: TJSONObject;
  begin
    Action := TJSONObject.Create;
    Action.Add('conditionParam1', 0);
    Action.Add('conditionParam2', 0);
    Action.Add('conditionType', 0);
    Action.Add('rating', 5);
    Action.Add('skillId', 1);

    CreateActions := TJSONArray.Create([Action]);
  end;

  function CreateDropItems: TJSONArray;
  var
    Items: TJSONArray;
    Item: TJSONObject;
    I: Integer;
  begin
    Items := TJSONArray.Create();

    for I := 1 to 3 do begin
      Item := TJSONObject.Create;
      Item.Add('dataId', 1);
      Item.Add('denominator', 1);
      Item.Add('kind', 0);
      Items.Add(Item);
    end;

    CreateDropItems := Items
  end;

  function CreateTraits: TJSONArray;
  var
    Traits: TJSONArray;
  begin
    Traits := TJSONArray.Create;
    Traits.Add(TJSONObject.Create(['code', TRAIT_XPARAM, 'dataId', 0, 'value', 0.95]));
    Traits.Add(TJSONObject.Create(['code', TRAIT_XPARAM, 'dataId', 1, 'value', 0.05]));
    Traits.Add(TJSONObject.Create(['code', TRAIT_ATTACK_ELEMENT, 'dataId', 1, 'value', 0]));

    CreateTraits := Traits;
  end;

begin
  NewEnemy := TJSONObject.Create;
  NewEnemy.Add('id', ID);
  NewEnemy.Add('actions', CreateActions);
  NewEnemy.Add('battlerHue', 0);
  NewEnemy.Add('battlerName', '');
  NewEnemy.Add('dropItems', CreateDropItems);
  NewEnemy.Add('exp', 0);
  NewEnemy.Add('traits', CreateTraits);
  NewEnemy.Add('gold', 0);
  NewEnemy.Add('name', '');
  NewEnemy.Add('note', '');
  NewEnemy.Add('params', TJSONArray.Create([100, 0, 10, 10, 10, 10, 10, 10]));

  CreateEmptyEnemy := NewEnemy;
end;

procedure TDatabase.ResizeTroops(NewMaximum: Integer);
begin
  ResizeDbArray(Troops, @CreateEmptyTroop, NewMaximum);
end;

function TDatabase.CreateEmptyTroop(ID: Integer): TJSONObject;
var
  NewTroop: TJSONObject;
begin
  NewTroop := TJSONObject.Create;
  NewTroop.Add('id', ID);
  NewTroop.Add('members', TJSONArray.Create);
  NewTroop.Add('name', '');
  NewTroop.Add('pages', TJSONArray.Create([CreateEmptyBattleEventPage]));

  CreateEmptyTroop := NewTroop;
end;

procedure TDatabase.ResizeStates(NewMaximum: Integer);
begin
  ResizeDbArray(States, @CreateEmptyState, NewMaximum);
end;

function TDatabase.CreateEmptyTileset(ID: Integer): TJSONObject;
var
  NewTileset: TJSONObject;

  function CreateFlags: TJSONArray;
  var
    Flags: TJSONArray;
    I: Integer;
  begin
    Flags := TJSONArray.Create;
    for I := 0 to TILE_ID_MAX do
      Flags.Add(GetDefaultTileFlags(I));

    CreateFlags := Flags
  end;

  function CreateTilesetNames: TJSONArray;
  var
    TilesetNames: TJSONArray;
    I: Integer;
  begin
    TilesetNames := TJSONArray.Create;
    for I := 0 to 8 do begin
      TilesetNames.Add('');
    end;

    CreateTilesetNames := TilesetNames
  end;

begin
  NewTileset := TJSONObject.Create;
  NewTileset.Add('id', ID);
  NewTileset.Add('flags', CreateFlags);
  NewTileset.Add('mode', 0); //0 area, 1 world
  NewTileset.Add('name', '');
  NewTileset.Add('note', '');
  NewTileset.Add('tilesetNames', CreateTilesetNames);

  CreateEmptyTileset := NewTileset;
end;

procedure TDatabase.ResizeTilesets(NewMaximum: Integer);
begin
  ResizeDbArray(Tilesets, @CreateEmptyTileset, NewMaximum);
end;

function TDatabase.CreateEmptyCommonEvent(ID: Integer): TJSONObject;
var
  NewCEv: TJSONObject;
begin
  NewCEv := TJSONObject.Create;
  NewCEv.Add('id', ID);
  NewCEv.Add('list', CreateEmptyECList);
  NewCEv.Add('name', '');
  NewCEv.Add('switchId', 1);
  NewCEv.Add('trigger', 0);

  CreateEmptyCommonEvent := NewCEv;
end;

procedure TDatabase.ResizeCommonEvents(NewMaximum: Integer);
begin
  ResizeDbArray(CommonEvents, @CreateEmptyCommonEvent, NewMaximum);
end;

function TDatabase.CreateEmptyState(ID: Integer): TJSONObject;
var
  NewState: TJSONObject;
begin
  NewState := TJSONObject.Create([
    'id', ID,
    'autoRemovalTiming', 0,
    'chanceByDamage', 100,
    'traits', TJSONArray.Create,
    'iconIndex', 0,
    'maxTurns', 1,
    'message1', '',
    'message2', '',
    'message3', '',
    'message4', '',
    'minTurns', 1,
    'motion', 0,
    'name', '',
    'note', '',
    'overlay', 0,
    'priority', 50,
    'removeAtBattleEnd', False,
    'removeByDamage', False,
    'removeByRestriction', False,
    'removeByWalking', False,
    'restriction', 0,
    'stepsToRemove', 100
  ]);

  CreateEmptyState := NewState;
end;

function TDatabase.GetNames(Arr: TJSONArray): TStringList;
var
  Names: TStringList;
  I: Integer;
begin
  Names := TStringList.Create;
  Names.Capacity := Arr.Count - 1;

  Names.BeginUpdate;
  for I := 1 to Arr.Count - 1 do begin
    Names.Add(TDatabase.FormatObjName(Arr.Objects[I]));
  end;
  Names.EndUpdate;

  GetNames := Names;
end;

class function TDatabase.FormatObjName(Obj: TJSONObject; UseLongName: Boolean = True): String;
var
  Id: TJSONNumber;
  ObjName: TJSONString;

  IdString, NameString: String;
begin
  IdString := '[???]';
  if Obj.Find('id', Id) then
    IdString := IntToStr(Id.AsInteger);

  NameString := '???';
  if Obj.Find('name', ObjName) then
    NameString := ObjName.AsString;

  if UseLongName then
    FormatObjName := IdString + ' ' + NameString
  else
    FormatObjName := NameString
end;


function TDatabase.GetActorDataIdsForTrait(ActorId: Integer; TraitCode: Integer): TIntList;
var
  ResultArray: TIntList;

  { Goes through the trait array, finds traits with a given code and
   adds dataId to ResultArray, unless it's already present in that array. }
  procedure AddDataIdsFromTraits(Obj: TJSONObject);
  var
    TraitIndex, ItemIndex, DataId: Integer;
    Trait: TJSONObject;
    TraitArray: TJSONArray;
  begin
    if Obj.Find('traits', TraitArray) then begin
      for TraitIndex := 0 to TraitArray.Count -1 do begin
        Trait := TraitArray.Objects[TraitIndex];
        if Trait.Integers['code'] = TraitCode then begin
          DataId := Trait.Integers['dataId'];
          ItemIndex := ResultArray.IndexOf(DataId);
          if ItemIndex = -1 then
            ResultArray.Add(DataId);
        end;
      end;
    end;
  end;

var
  Actor: TJSONObject;
  ClassId: TJSONNumber;
begin
  ResultArray := TIntList.Create;

  if ActorId < Actors.Count then begin
    Actor := Actors.Objects[ActorId];

    AddDataIdsFromTraits(Actor);
    if Actor.Find('classId', ClassId) then begin
      if ClassId.AsInteger < Classes.Count then
        AddDataIdsFromTraits(Classes.Objects[ClassId.AsInteger]);
    end;

    (*
     * Equipment is not handled, because:
     * - This would require re-creating equipment lists each time a equipment
     * is changed: it can get pretty expensive.
     * - This would require checking dual-wield each time (and this can also
     * depend on the other weapons equipped... which can quickly become too hard
     * to track).
     * - RPG Maker MV doesn't check equipment traits when determining initial
     * equpment, so we don't need to be holier than the pope.
     *)
  end;

  GetActorDataIdsForTrait := ResultArray;
end;

function TDatabase.GetActorWeaponTypes(ActorId: Integer): TIntList;
begin
  GetActorWeaponTypes := GetActorDataIdsForTrait(ActorId, TRAIT_EQUIP_WTYPE);
end;

function TDatabase.GetActorArmourTypes(ActorId: Integer): TIntList;
begin
  GetActorArmourTypes := GetActorDataIdsForTrait(ActorId, TRAIT_EQUIP_ATYPE);
end;

function TDatabase.ActorHasTwoWeapons(ActorId: Integer): Boolean;
var
  SlotTypes: TIntList;
  I: Integer;
  IsDualWield: Boolean;
begin
  SlotTypes := GetActorDataIdsForTrait(ActorId, TRAIT_SLOT_TYPE);
  IsDualWield := False;
  for I := 0 to SlotTypes.Count -1 do begin
    if SlotTypes[I] = 1 then
      IsDualWield := True;
  end;

  SlotTypes.Free;
  ActorHasTwoWeapons := IsDualWield;
end;

function TDatabase.GetNameFromSystemArray(ArrayName: String; Id: Integer): String;
var
  Names: TJSONArray;
  Name: String = '???';
begin
  Assert(System <> nil);
  if System.Find(ArrayName, Names) then begin
    if (Id >= 1) and (Id < Names.Count) then begin
      Name := ' ' + Names.Strings[Id];
    end;
  end;

  GetNameFromSystemArray := '#' + IntToStr(Id) + Name
end;

function TDatabase.GetNameFromDbArray(Arr: TJSONArray; Id: Integer;
  UseLongName: Boolean = True): String;
begin
  Assert(Arr <> nil);

  if (Id >= 1) and (Id < Arr.Count) then
    GetNameFromDbArray := TDatabase.FormatObjName(Arr.Objects[Id], UseLongName)
  else
    GetNameFromDbArray := '#' + IntToStr(Id);
end;

function TDatabase.GetElementName(Id: Integer): String;
begin
  GetElementName := GetNameFromSystemArray('elements', Id)
end;

function TDatabase.GetParamName(Id: Integer): String;
var
  Terms: TJSONObject;
  ParamNames: TJSONArray;
  Name: String = '';
begin
  Assert(System <> nil);

  if System.Find('terms', Terms) then begin
    if Terms.Find('params', ParamNames) then begin
      if (Id >= 0) and (Id < ParamNames.Count - 1) then begin
        Name := ParamNames.Strings[Id];
      end;
    end;
  end;

  if Name <> '' then
    GetParamName := Name
  else
    GetParamName := 'Param#' + IntToStr(Id);
end;

function TDatabase.GetArmourTypeName(Id: Integer): String;
begin
  GetArmourTypeName := GetNameFromSystemArray('armorTypes', Id)
end;

function TDatabase.GetWeaponTypeName(Id: Integer): String;
begin
  GetWeaponTypeName := GetNameFromSystemArray('weaponTypes', Id)
end;

function TDatabase.GetVariableName(Id: Integer): String;
begin
  GetVariableName := GetNameFromSystemArray('variables', Id)
end;

function TDatabase.GetSwitchName(Id: Integer): String;
begin
  GetSwitchName := GetNameFromSystemArray('switches', Id)
end;

function TDatabase.GetEquipTypeName(Id: Integer): String;
begin
  GetEquipTypeName := GetNameFromSystemArray('equipTypes', Id)
end;

function TDatabase.GetSkillTypeName(Id: Integer): String;
begin
  GetSkillTypeName := GetNameFromSystemArray('skillTypes', Id)
end;

function TDatabase.GetClassName(Id: Integer; UseLongName: Boolean): String;
begin
  GetClassName := GetNameFromDbArray(Classes, Id, UseLongName);
end;

function TDatabase.GetTilesetName(Id: Integer; UseLongName: Boolean): String;
begin
  GetTilesetName := GetNameFromDbArray(Tilesets, Id, UseLongName);
end;

function TDatabase.GetStateName(Id: Integer): String;
begin
  if Id = 0 then
    GetStateName := NormalAttackState
  else
    GetStateName := GetNameFromDbArray(States, Id);
end;

function TDatabase.GetSkillName(Id: Integer; UseLongName: Boolean = True): String;
begin
  GetSkillName := GetNameFromDbArray(Skills, Id, UseLongName);
end;

function TDatabase.GetCommonEventName(Id: Integer; UseLongName: Boolean
  ): String;
begin
  GetCommonEventName := GetNameFromDbArray(CommonEvents, Id, UseLongName);
end;

function TDatabase.GetAnimName(Id: Integer; UseLongName: Boolean): String;
begin
  GetAnimName := GetNameFromDbArray(Animations, Id, UseLongName);
end;

function TDatabase.GetActorName(Id: Integer; UseLongName: Boolean): String;
begin
  GetActorName := GetNameFromDbArray(Actors, Id, UseLongName);
end;

function TDatabase.GetItemName(Id: Integer; UseLongName: Boolean): String;
begin
  GetItemName := GetNameFromDbArray(Items, Id, UseLongName);
end;

function TDatabase.GetArmourName(Id: Integer; UseLongName: Boolean): String;
begin
  GetArmourName := GetNameFromDbArray(Armours, Id, UseLongName);
end;

function TDatabase.GetWeaponName(Id: Integer; UseLongName: Boolean): String;
begin
  GetWeaponName := GetNameFromDbArray(Weapons, Id, UseLongName);
end;

function TDatabase.GetEnemyName(Id: Integer; UseLongName: Boolean): String;
begin
  GetEnemyName := GetNameFromDbArray(Enemies, Id, UseLongName);
end;

function TDatabase.GetTroopName(Id: Integer; UseLongName: Boolean): String;
begin
  GetTroopName := GetNameFromDbArray(Troops, Id, UseLongName);
end;

function TDatabase.GetExParamName(Id: Integer): String;
begin
  if (Id >= Low(ExParamNames)) and (Id <= High(ExParamNames)) then
    GetExParamName := ExParamNames[Id]
  else
    GetExParamName := 'ExParam#' + IntToStr(Id);
end;

function TDatabase.GetSpParamName(Id: Integer): String;
begin
  if (Id >= Low(SpParamNames)) and (Id <= High(SpParamNames)) then
    GetSpParamName := SpParamNames[Id]
  else
    GetSpParamName := 'SpParam#' + IntToStr(Id);
end;

function TDatabase.GetSpecialFlagName(Id: Integer): String;
begin
  if (Id >= Low(SpecialFlagNames)) and (Id <= High(SpecialFlagNames)) then
    GetSpecialFlagName := SpecialFlagNames[Id]
  else
    GetSpecialFlagName := 'SpecialFlag#' + IntToStr(Id);
end;

function TDatabase.GetKnockoutEffectName(Id: Integer): String;
begin
  if (Id >= Low(KnockoutEffectFlagNames)) and (Id <= High(KnockoutEffectFlagNames)) then
    GetKnockoutEffectName := KnockoutEffectFlagNames[Id]
  else
    GetKnockoutEffectName := 'KnockoutEffect#' + IntToStr(Id);
end;

function TDatabase.GetPartyFlagName(Id: Integer): String;
begin
  if (Id >= Low(PartyFlagFlagNames)) and (Id <= High(PartyFlagFlagNames)) then
    GetPartyFlagName := PartyFlagFlagNames[Id]
  else
    GetPartyFlagName := 'PartyFlag#' + IntToStr(Id);
end;

function TDatabase.IsSvBattle: Boolean;
var
  SideViewJson: TJSONBoolean;
begin
  IsSvBattle := False;

  if (System <> nil) and System.Find('optSideView', SideViewJson) then
    IsSvBattle := SideViewJson.AsBoolean;
end;

destructor TDatabase.Destroy;
begin
  if Actors <> nil then
    Actors.Free;
  if Classes <> nil then
    Classes.Free;
  if Skills <> nil then
    Skills.Free;
  if Items <> nil then
    Items.Free;
  if Weapons <> nil then
    Weapons.Free;
  if Armours <> nil then
    Armours.Free;
  if Enemies <> nil then
    Enemies.Free;
  if Troops <> nil then
    Troops.Free;
  if States <> nil then
    States.Free;
  if Animations <> nil then
    Animations.Free;
  if Tilesets <> nil then
    Tilesets.Free;
  if CommonEvents <> nil then
    CommonEvents.Free;
  if System <> nil then
    System.Free;

  inherited;
end;

end.

